//
// Created by LiuYou on 2021/8/20.
//

#include <sys/epoll.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/mman.h>
#include <sys/uio.h>
#include <cstdarg>
#include <cerrno>


#include "HttpConnect2.hpp"

//服务端资源页的路径，将其和HTTP请求中解析的m_url拼接形成资源页的位置
static const char* DOC_ROOT = "/var/www/html";


static const char* ERROR_500_TITLE = "Internal Error";

static const char* ERROR_500_FORM = "There was an unusual problem serving the requested file.\n";


static const char* ERROR_400_TITLE = "Bad Request";

static const char* ERROR_400_FORM = "Your request has bad syntax or is inherently impossible to satisfy.\n";


static const char* ERROR_404_TITLE = "Not Found";

static const char* ERROR_404_FORM = "The requested file was not found on this server.\n";


static const char* ERROR_403_TITLE = "Forbidden";

static const char* ERROR_403_FORM = "Forbidden";


static const char* OK_200_TITLE = "OK";


int HttpConnect::epollFd = -1;
int HttpConnect::userCount = 0;


////////////////////////////////////////////////////////////////////////////////////

/*static*/ int setFdNonblocking(int fd) {
    int oldOption = fcntl( fd, F_GETFL );
    // 疑惑解开, 将文件描述符设置为非阻塞, 使用的是宏 O_NONBLOCK。
    int newOption = oldOption | O_NONBLOCK;
    fcntl( fd, F_SETFL, newOption );
    return oldOption;
}

/*static*/ void addFdToEpoll(int epollFd, int fd, bool oneShot) {
    epoll_event epollEvent{};
    epollEvent.data.fd = fd;
    epollEvent.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    if (oneShot) {
        epollEvent.events |= EPOLLONESHOT;
    }
    setFdNonblocking( fd );
    epoll_ctl( epollFd, EPOLL_CTL_ADD, fd, &epollEvent );
}


/*static*/ void removeFd(int epollFd, int fd) {
    epoll_ctl( epollFd, EPOLL_CTL_DEL, fd, nullptr );
    close( fd );
}

/*static*/ void modFd(int epollFd, int fd, EPOLL_EVENTS events) {
    epoll_event epollEvent{};
    epollEvent.data.fd = fd;
    epollEvent.events = events | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    epoll_ctl( epollFd, EPOLL_CTL_MOD, fd, &epollEvent );
}




///////////////////////////////////////////////////////////////////////////


HttpConnect::HttpConnect() = default;

HttpConnect::~HttpConnect() = default;


/**
 * @brief 重载init函数进行些连接前的初始化操作。
 */
void HttpConnect::init() {
    checkState = CheckState::CHECK_STATE_REQUEST_LINE;
    linger = false;

    method = Method::GET;
    url = nullptr;
    version = nullptr;
    contentLength = 0;
    host = nullptr;
    startLine = 0;
    checkedIdx = 0;
    readIdx = 0;
    writeIdx = 0;

    memset( readBuf, '\0', READ_BUFFER_SIZE );
    memset( writeBuf, '\0', WRITE_BUFFER_SIZE );
    memset( realFile, '\0', FILE_NAME_LEN );
}


/**
 * @brief 初始化连接
 * @param connectionFd
 * @param address
 */
void HttpConnect::init(int connectionFd, const sockaddr_in& address) {
    //connectFd是http连接对应的描述符用于接收http请求和http回答
    connectFd = connectionFd;
    //客户端地址
    remoteAddress = address;

    /** 如下两行是为了避免 TIME_WAIT状态, 仅用于调试, 实际使用时应该去掉。*/
    int reuse = 1;
    setsockopt( connectFd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ));
    addFdToEpoll( epollFd, connectFd, true );
    //多了一个http用户
    ++userCount;

    init();
}


/**
 * @brief 关闭连接，从事件表中移除描述符。
 * @param realClose
 */
void HttpConnect::closeConnection(bool realClose) {
    if (realClose && (connectFd != -1)) {
        removeFd( epollFd, connectFd );
        connectFd = -1;
        /** 关闭一个连接时, 将客户总量减 1。*/
        --userCount;
    }
}


/**
 * @brief 有线程池中的工作线程调用, 这是处理HTTP请求的入口函数。
 */
void HttpConnect::process() {
    HttpCode readRet = processRead();
    if (readRet == HttpCode::NO_REQUEST) {
        modFd( epollFd, connectFd, EPOLLIN );
        return;
    }
    bool writeRet = processWrite( readRet );
    if (!writeRet) {
        closeConnection();
    }
    modFd( epollFd, connectFd, EPOLLOUT );
}


/**
 * @brief 循环读取客户数据, 直到无数据可读或者对方关闭连接。
 * @return
 */
bool HttpConnect::read() {
    if (readIdx >= READ_BUFFER_SIZE) {
        return false;
    }

    while (true) {
        ssize_t rcvByteNum = recv( connectFd, readBuf + readIdx, READ_BUFFER_SIZE - readIdx, 0 );
        if (rcvByteNum == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break;
            }
            return false;
        } else if (rcvByteNum == 0) {
            return false;
        }
        readIdx += rcvByteNum;
    }
    return true;
}


/**
 * @brief 写HTTP响应(response)。
 * <p>
 * 将资源页文件发送给客户端
 * @return
 */
bool HttpConnect::write() {
    ssize_t sedBytes = writeIdx;
    if (sedBytes == 0) {
        modFd( epollFd, connectFd, EPOLLIN );
        init();
        return true;
    }

    ssize_t bytesHaveSend = 0;
    while (true) {
        //集中写，connectFd是http连接对应的描述符，ioVector是iovec结构体数组表示内存块地址，ioVectorCount是数组的长度即多少个内存块将一次集中写到connectFd
        ssize_t readBytesNum = writev( connectFd, ioVector, ioVectorCount );
        //集中写失败
        if (readBytesNum <= -1) {
            /* 如果TCP写缓冲没有空间, 则等待下一轮EPOLLOUT事件。虽然在此期间, 服务器无法立刻接受到同一客户的下一个请求,
             * 但这可以保证连接的完整性。*/
            if (errno == EAGAIN) {
                //重置EPOLLONESHOT事件,注册可写事件表示若connectFd没有写失败则关闭连接
                modFd( epollFd, connectFd, EPOLLOUT );
                return true;
            }
            //解除内存映射, 回收内存。
            unmap();
            return false;
        }
        //待发送数据
        sedBytes -= readBytesNum;
        //已发送数据
        bytesHaveSend += readBytesNum;
        if (sedBytes <= bytesHaveSend) {
            /* 发送HTTP响应成功, 根据HTTP请求中的Connection字段决定是否李隆基关闭连接。*/
            //该资源页已经发送完毕该解除映射
            unmap();
            //若要保持该http连接
            if (linger) {
                //初始化http连接
                init();
                modFd( epollFd, connectFd, EPOLLIN );
                return true;
            } else {
                modFd( epollFd, connectFd, EPOLLIN );
                return false;
            }
        }
    }
}


/**
 * @brief 主状态机。
 * @return
 */
HttpCode HttpConnect::processRead() {
    LineStatus lineStatus = LineStatus::LINE_OK;
    char* text = nullptr;

    while (((checkState == CheckState::CHECK_STATE_CONTENT) &&
            (lineStatus == LineStatus::LINE_OK)) ||
           ((lineStatus = parseLine()) == LineStatus::LINE_OK)) {
        text = getLine();
        startLine = checkedIdx;
//        printf( "got 1 http line: %s\n", text );
        printf( "得到 1 http 行: %s\n", text );

        switch (checkState) {
            case CheckState::CHECK_STATE_REQUEST_LINE: {
                HttpCode httpCode = parseRequestLine( text );
                if (httpCode == HttpCode::BAD_REQUEST) {
                    return HttpCode::BAD_REQUEST;
                }
                break;
            }
            case CheckState::CHECK_STATE_HEADER: {
                HttpCode httpCode = parseHeaders( text );
                if (httpCode == HttpCode::BAD_REQUEST) {
                    return HttpCode::BAD_REQUEST;
                } else if (httpCode == HttpCode::GET_REQUEST) {
                    return doRequest();
                }
                break;
            }
            case CheckState::CHECK_STATE_CONTENT: {
                HttpCode httpCode = parseContent( text );
                if (httpCode == HttpCode::GET_REQUEST) {
                    return doRequest();
                }
                lineStatus = LineStatus::LINE_OPEN;
                break;
            }
            default: {
                return HttpCode::INTERNAL_ERROR;
            }
        }
    }

    return HttpCode::NO_REQUEST;
}


/**
 * @brief 从状态机。用于解析出一行内容。
 * @return
 */
LineStatus HttpConnect::parseLine() {
    char temp;
    //checked_index指向buffer（应用程序读缓冲区）中当前正在分析的字节，read_index指向buffer中客户数据尾部的下一个字节;
    //buffer中的第0 ~ checked_index字节都已分析完毕, 第checked_index ~ (read_index - 1)字节由下面的循环挨个分析
    for (; checkedIdx < readIdx; ++checkedIdx) {
        //获得当前要分析的字节；
        temp = readBuf[checkedIdx];
        //如果\r恰巧是目前buffer中最后一个已经被读入的客户数据，那么这次分析没有读取到完整的行，
        //返回LINE_OPEN表示还需要继续读取客户数据来进一步分析
        if (temp == '\r') {
            if ((checkedIdx + 1) == readIdx) {
                return LineStatus::LINE_OPEN;
            }
                //如果下一个字符是\n,说明读到一个完整的行
            else if (readBuf[checkedIdx + 1] == '\n') {
                // 终于找到错的地方了。
                readBuf[checkedIdx++] = '\0';
                readBuf[checkedIdx++] = '\0';
                return LineStatus::LINE_OK;
            }
            //否则，说明存在语法问题
            return LineStatus::LINE_BAD;
            //如果当前字符是\n，说明也可能读到一个完整的行
            //not necessary
        } else if (temp == '\n') {
            if ((checkedIdx > 1) && (readBuf[checkedIdx - 1] == '\r')) {
                readBuf[checkedIdx - 1] = '\0';
                readBuf[checkedIdx++] = '\0';
                return LineStatus::LINE_OK;
            }
            return LineStatus::LINE_BAD;
        }
    }
    //如果所有内容分析完毕都没有遇到\r字符，则返回LINE_OPEN，表示还需要继续读取客户数据才能进一步分析
    return LineStatus::LINE_OPEN;
}

char* HttpConnect::getLine() {
    return readBuf + startLine;
}


HttpCode HttpConnect::parseRequestLine(char* text) {
    //strpbrk()函数检索两个字符串中首个相同字符的位置，其原型为：
    //    char *strpbrk( char *s1, char *s2)
    url = strpbrk( text, " \t" );
    //请求行中如果没有\t，则该HTTP请求有问题
    if (!url) {
        return HttpCode::BAD_REQUEST;
    }
    *url++ = '\0';

    char* method = text;
    //函数声明：int strcasecmp (const char *s1, const char *s2);
    //函数说明：strcasecmp()用来比较参数s1 和s2 字符串，比较时会自动忽略大小写的差异。
    //返回值：若参数s1 和s2 字符串相同则返回0。s1 长度大于s2 长度则返回大于0 的值，s1 长度若小于s2 长度则返回小于0 的值。
    if (strcasecmp( method, "GET" ) == 0) {
        this->method = Method::GET;
    } else {
        return HttpCode::BAD_REQUEST;
    }

    //strspn() 函数用来计算字符串 str 中连续有几个字符都属于字符串 accept，其原型为：
    //size_t strspn(const char *str, const char * accept);
    url += strspn( url, " \t" );
    version = strpbrk( url, " \t" );
    if (!version) {
        return HttpCode::BAD_REQUEST;
    }
    *version++ = '\0';
    version += strspn( version, " \t" );
    if (strcasecmp( version, "HTTP/1.1" ) != 0) {
        return HttpCode::BAD_REQUEST;
    }
    if (strncasecmp( url, "http://", 7 ) == 0) {
        url += 7;
        //strchr() 用来查找某字符在字符串中首次出现的位置，其原型为：
        //char * strchr (const char *str, int c)
        url = strchr( url, '/' );
    }
    if (!url || url[0] != '/') {
        return HttpCode::BAD_REQUEST;
    }
    //HTTP请求行处理完毕，状态转移到头部字段的分析
    checkState = CheckState::CHECK_STATE_HEADER;
    return HttpCode::NO_REQUEST;
}


/**
 * @brief 解析http请求的一个头部信息。
 * @param text
 * @return
 */
HttpCode HttpConnect::parseHeaders(char* text) {
    //空行，头部字段解析完毕
    if (text[0] == '\0') {
        /* 如果HTTP请求有消息体, 则还需要读取contentLength字节的消息体, 状态机转移到 CHECK_STATE_CONTENT状态。*/
        if (contentLength != 0) {
            checkState = CheckState::CHECK_STATE_CONTENT;
            return HttpCode::NO_REQUEST;
        }
        /* 否则说明我们已经得到了一个完整的HTTP请求。*/
        return HttpCode::GET_REQUEST;
    }
        /* 处理Connection头部字段。*/
    else if (strncasecmp( text, "Connection:", 11 ) == 0) {
        text += 11;
        text += strspn( text, " \t" );
        if (strcasecmp( text, "keep-alive" ) == 0) {
            linger = true;
        }
    }
        /* 处理Content-Length头部字段。*/
    else if (strncasecmp( text, "Content-Length:", 15 ) == 0) {
        text += 15;
        text += strspn( text, " \t" );
        contentLength = atol( text );
    }
        /* 处理Host头部字段。*/
    else if (strncasecmp( text, "Host:", 5 ) == 0) {
        text += 5;
        text += strspn( text, " \t" );
        host = text;
    } else {
        printf( "Oops! unknown header %s\n", text );
    }
    return HttpCode::NO_REQUEST;
}


/**
 * @brief 当得到一个完整、正确的HTTP请求时, 我们就分析目标文件的属性。如果目标文件存在、对所有用户可读, 且不是目录,
 * 则使用 mmap() 将其映射到内存地址 fileAddress处, 并告诉调用者获取文件成功。
 * <p>
 * 用于获取资源页文件的状态。
 * @return
 */
HttpCode HttpConnect::doRequest() {
    strcpy( realFile, DOC_ROOT );
    size_t len = strlen( DOC_ROOT );
    strncpy( realFile + len, url, FILE_NAME_LEN - len - 1 );
    if (stat( realFile, &fileStat ) < 0) {
        //若资源页不存在则HTTP解析结果为404
        return HttpCode::NO_RESOURCE;
    }
    if (!(fileStat.st_mode & S_IROTH)) {
        //资源没有权限获取
        return HttpCode::FORBIDDEN_REQUEST;
    }
    if (S_ISDIR( fileStat.st_mode )) {
        //请求有错
        return HttpCode::BAD_REQUEST;
    }
    int fd = open( realFile, O_RDONLY );
    //将资源页文件映射到内存
    fileAddress = (char*) mmap( nullptr, fileStat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
    close( fd );
    //资源页请求成功
    return HttpCode::FILE_REQUEST;
}


/**
 * @brief 我们没有真正解析http请求的消息体, 只是判断它是否被完整地读入了。
 * <p>
 * 消息体不解析，判断一下是否完整读入。
 * @param text
 * @return
 */
HttpCode HttpConnect::parseContent(char* text) {
    if (readIdx >= (contentLength + checkedIdx)) {
        text[contentLength] = '\0';
        return HttpCode::GET_REQUEST;
    }
    return HttpCode::NO_REQUEST;
}


/**
 * @brief 根据服务器处理HTTP请求的结果, 决定返回给客户端的内容。
 * @param code
 * @return
 */
bool HttpConnect::processWrite(HttpCode code) {
    switch (code) {
        case HttpCode::INTERNAL_ERROR: {
            addStatusLine( 500, ERROR_500_TITLE );
            addHeaders( strlen( ERROR_500_FORM ));
            if (!addContent( ERROR_500_FORM )) {
                return false;
            }
            break;
        }
        case HttpCode::BAD_REQUEST: {
            addStatusLine( 400, ERROR_400_TITLE );
            addHeaders( strlen( ERROR_400_FORM ));
            if (!addContent( ERROR_400_FORM )) {
                return false;
            }
            break;
        }
        case HttpCode::NO_RESOURCE: {
            addStatusLine( 404, ERROR_404_TITLE );
            addHeaders( strlen( ERROR_404_FORM ));
            if (!addContent( ERROR_404_FORM )) {
                return false;
            }
            break;
        }
        case HttpCode::FORBIDDEN_REQUEST: {
            addStatusLine( 403, ERROR_403_TITLE );
            addHeaders( strlen( ERROR_403_FORM ));
            if (!addContent( ERROR_403_FORM )) {
                return false;
            }
            break;
        }
        case HttpCode::FILE_REQUEST: {
            addStatusLine( 200, OK_200_TITLE );
            if (fileStat.st_size != 0) {
                addHeaders( fileStat.st_size );
                ioVector[0].iov_base = writeBuf;
                ioVector[0].iov_len = writeIdx;
                ioVector[1].iov_base = fileAddress;
                ioVector[1].iov_len = fileStat.st_size;
                ioVectorCount = 2;
                return true;
            } else {
                const char* okStr = "<html><body></body></html>";
                addHeaders( strlen( okStr ));
                if (!addContent( okStr )) {
                    return false;
                }
            }
        }
        default: {
            return false;
        }
    }

    ioVector[0].iov_base = writeBuf;
    ioVector[0].iov_len = writeIdx;
    ioVectorCount = 1;
    return true;
}

bool HttpConnect::addStatusLine(int status, const char* title) {
    return addResponse( "%s %d %s\r\n", "HTTP/1.1", status, title );
}

/**
 * @brief 往写缓冲中写入待发送的数据。
 * <p>
 * HTTP应答主要是将应答数据添加到写缓冲区writeBuf。
 * @param format
 * @param ...
 * @return
 */
bool HttpConnect::addResponse(const char* format, ...) {
    if (writeIdx >= WRITE_BUFFER_SIZE) {
        return false;
    }
    va_list argList;
    va_start( argList, format );
    //将format内容输出到writeBuf
    int len = vsnprintf( writeBuf + writeIdx, WRITE_BUFFER_SIZE - 1 - writeIdx, format, argList );
    if (len >= (WRITE_BUFFER_SIZE - 1 - writeIdx)) {
        return false;
    }
    writeIdx += len;
    va_end( argList );
    return true;
}

bool HttpConnect::addHeaders(size_t contentLength) {
    addContentLength( contentLength );
    addLinger();
    addBlankLine();
//    return false;
}

bool HttpConnect::addContentLength(size_t contentLength) {
    return addResponse( "Content-Length: %d\r\n", contentLength );
}

bool HttpConnect::addLinger() {
    return addResponse( "Connection: %s\r\n", linger ? "keep-alive" : "close" );
}

bool HttpConnect::addBlankLine() {
    return addResponse( "%s", "\r\n" );
}

bool HttpConnect::addContent(const char* content) {
    return addResponse( "%s", content );
}

/**
 * @brief 对内存映射区执行 munmap() 操作。
 * <p>
 * 解除资源页文件映射的内存。
 */
void HttpConnect::unmap() {
    if (fileAddress) {
        //解除映射
        munmap( fileAddress, fileStat.st_size );
        fileAddress = nullptr;
    }
}


